// 15 面向对象程序设计
/**
 * 面向对象程序设计基于三个基本概念：数据抽象、继承和动态绑定。第7章已经介绍了数据抽象的知识，本章将介绍继承和动态绑定。
 * 继承和动态绑定对程序的编写有两方面的影响：一是我们可以更容易地定义与其他类相似但不完全相同的新类；二是在使用这些彼此相似的类编写程序时，我们可以在一定程度上忽略掉它们的区别。
 * 在很多程序中都存在着一些相互关联但是有细微差别的概念。
 * 例如，书店中不同书籍的定价策略可能不同：有的书籍按原价销售，有的则打折销售。有时，我们给那些购买书籍超过一定数量的顾客打折；另一些时候，则只对前多少本销售的书籍打折，之后就调回原价，等等。
 * 面向对象的程序设计（OOP）适用于这类应用。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;
#include "../Chapter13/13.5.cc" // 不能编译，因为重复定义的main函数
#include "../Chapter12/12.1.6.cc" // 不能编译，因为重复定义的main函数
#include <functional>

struct Base
{
    Base() : mem(0) {}
protected:
    int mem; // protected成员
};
struct Derived : public Base{
    Derived(int i) : mem(i) {}   // 用i初始化
    int get_mem() {return mem;}
protected:
    int mem;
};

struct Base1
{
    int memfun();
};
struct Derived1 : public Base1
{
    int memfun(int); // 隐藏基类的memfun
};

class Base2
{
public:
    virtual int fcn();
};
class D1 : public Base2
{
public:
    int fcn(int); // 形参列表与Base2中的不一致
    virtual void f2(); // 是一个新的虚函数，在Base2中不存在
};
class D2 : public D1
{
public:
    int fcn(int); // 是一个虚函数，隐藏了D1:fcn(int)
    int fcn();    // 覆盖了Base2的虚函数fcn
    void f2();    // 覆盖了D1的虚函数f2
};

int main()
{
    // 15.6 继承中的类作用域
    // 如果一个名字在派生类的作用域内无法正确解析，则编译器将继续在外层的基类作用域中寻找该名字的定义。
    // 派生类的作用域位于基类作用域之内这一事实可能有点儿出人意料，毕竟在我们的程序文本中派生类和基类的定义是相互分离开来的。
    // 不过也恰恰因为类作用域有这种继承嵌套的关系，所以派生类才能像使用自己的成员一样使用基类的成员。
    
    // 在编译时进行名字查找
    // 一个对象、引用或指针的静态类型（参见15.2.3节，第532页）决定了该对象的哪些成员是可见的。
    // 静态类型在编译时已知，它是变量声明时的类型或者表达式生成的类型。动态类型则是变量或表达式表示的内存中的对象的类型，动态类型在运行时才可知。
    // 即使静态类型与动态类型可能不一致（当使用基类的引用或指针时会发生这种情况），但是我们能使用哪些成员仍然是由静态类型决定的。

    // 名字冲突与继承
    // 和其他作用域一样，派生类也能重用定义在其直接基类或间接基类中的名字，此时定义在内层作用域（即派生类）的名字将隐藏定义在外层作用域（即基类）的名字（参见2.2.4节，第43页）：
    // 派生类的成员将隐藏同名的基类成员。

    // 通过作用域运算符来使用隐藏的成员
    // 我们可以通过作用域运算符来使用一个被隐藏的基类成员：
    //int get_mem() {return Base::mem;}
    // 除了覆盖继承而来的虚函数之外，派生类最好不要重用其他定义在基类中的名字。

    // 一如往常，名字查找先于类型检查
    // 如前所述，声明在内层作用域的函数并不会重载声明在外层作用域的函数（参见6.4.1节，第210页）。因此，定义派生类中的函数也不会重载其基类中的成员。
    // 和其他作用域一样，如果派生类（即内层作用域）的成员与基类（即外层作用域）的某个成员同名，则派生类将在其作用域内隐藏该基类成员。
    // ***即使派生类成员和基类成员的形参列表不一致，基类成员也仍然会被隐藏掉：***
    Derived1 d; Base1 b;
    b.memfun(); // 调用Base1::memfun
    d.memfun(10); // 调用Drived1:memfun
    //d.memfun(); // 错误，参数列表为空的memfun被隐藏了
    d.Base1::memfun(); // 正确，调用Base1:memfun

    // 虚函数与作用域
    // 我们现在可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了（参见15.3节，第537页）。
    // 假如基类与派生类的虚函数接受的实参不同，则我们就无法通过基类的引用或指针调用派生类的虚函数了。

    // 通过基类调用隐藏的虚函数
    Base2 bobj; D1 d1obj; D2 d2obj;
    Base2 *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
    bp1->fcn(); // 虚调用，将在运行时调用Base2::fcn
    bp2->fcn(); // 虚调用，将在运行时调用Base2::fcn
    bp3->fcn(); // 虚调用，将在运行时调用D2::fcn
    D1 *d1p = &d1obj; D2 *d2p = &d2obj;
    //bp2->f2(); // 错误，Base2没有名为f2的成员
    d1p->f2(); // 虚调用，将在运行时调用D1::f2
    d2p->f2(); // 虚调用，将在运行时调用D2::f2

    // 覆盖重载的函数
    // 和其他函数一样，成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的，那么它就需要覆盖所有的版本，或者一个也不覆盖。
    // 一种好的解决方案是为重载的成员提供一条using声明语句（参见15.5节，第546页），这样我们就无须覆盖基类中的每一个重载版本了。
    // using声明语句指定一个名字而不指定形参列表，所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。
    // 此时，派生类只需要定义其特有的函数就可以了，而无须为继承而来的其他函数重新定义。
    // 类内using声明的一般规则同样适用于重载函数的名字（参见15.5节，第546页）；基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。

    return 0;
}